﻿using anydata.Entitys;
using anydata.Loaders.ResponseModel;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json.Linq;

namespace anydata.Services;

public class ThingDepreciationService : ITransient
{
    private readonly ILogger _logger;
    private readonly DataProvider provider;
    private readonly ThingLockService lockService;

    public ThingDepreciationService(DataProvider _provider, ThingLockService _lockService, ILogger<DataProvider> logger)
    {
        _logger = logger;
        this.provider = _provider;
        this.lockService = _lockService;
    }

    /// <summary>
    /// 折旧操作
    /// </summary>  
    public async Task<OperateResult> DepreciationEnterAsync(TokenModel token, DepreciationArgs args)
    {
        var result = await CreateContext(token, args);
        if (result.success)
        {
            switch (args.Type)
            {
                case "Calculate":
                    RunTask(result.data, () => CalculateOrConfirm(result.data, false));
                    break;
                case "Confirm":
                    RunTask(result.data, () => CalculateOrConfirm(result.data, true));
                    break;
                case "Revoke":
                    RunTask(result.data, () => Revoke(result.data));
                    break;
                case "Close":
                    RunTask(result.data, () => Close(result.data));
                    break;
                default:
                    return OperateResult.Faild($"未知的折旧操作，${args.Type}！");
            }
            return OperateResult.Success(result.data.OperationLog);
        }
        return result;
    }

    /// <summary>
    /// 记录表更
    /// </summary>  
    public async Task<(List<XChange>, List<XSnapshot>)> RecordAllChangesAsync(CompareContext context, List<EntityBase> things)
    {
        var changes = new List<XChange>();
        var snapshots = new List<XSnapshot>();
        if (context.HasRecord)
        {
            var options = BuildOptions(context, things.Select(item => item.id).ToList());
            var result = await provider.LoadAsync(context.Token, DataProvider.ThingCollName, options);
            if (!result.success)
            {
                return (changes, snapshots);
            }
            var oldThings = ((result.data as LoadResult)?.data as List<EntityBase>) ?? new List<EntityBase>();
            foreach (var newThing in things)
            {
                var oldThing = oldThings.Find(a => a.id == newThing.id);
                var snapshot = new XSnapshot(oldThing ?? newThing, context.Instance);
                changes.AddRange(XChange.CompareThing(context, oldThing, newThing.isDeleted == true ? null : newThing, snapshot.id));
                snapshots.Add(snapshot);
            }
        }
        return (changes, snapshots);
    }

    /// <summary>
    /// 解锁、保存快照
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private async Task Close(DepreciationContext context)
    {
        await provider.UpdateAsync(context.Token, DataProvider.ThingCollName, new CollectUpdateData()
        {
            Match = new JObject
            {
                { "locks.exclusion.id", JValue.CreateString(context.Current.id) }
            },
            Update = new JObject
            {
                { "_unset_", new JObject {
                    {  "locks", JValue.CreateString("") }
                } }
            }
        });
        await provider.SnapshotAsync(context.Token, DataProvider.ThingCollName, context.Current.Period);
    }

    private void RunTask(DepreciationContext context, Func<Task> task)
    {
        Task.Run(async () =>
        {
            await provider.UpdateOperationId(context.Token, context.Current.id, context.OperationLog.id);
            await provider.UpdateOperationProgress(context.Token, context.OperationLog.id, 0, 0);
            await provider.UpdateOperationStatus(context.Token, context.OperationLog.id, (int)Status.Working);
            await task();

        }).ContinueWith(async (task) =>
        {
            await provider.UpdateOperationId(context.Token, context.Current.id, null);
            if (task.Exception != null)
            {
                await provider.UpdateOperationStatus(context.Token, context.OperationLog.id, (int)Status.Error);
                await provider.UpdateOperationRemark(context.Token, context.OperationLog.id, task.Exception.Message);
            }
            else
            {
                await provider.UpdateOperationStatus(context.Token, context.OperationLog.id, (int)Status.Completed);
            }
        });
    }

    private async Task<OperateResult<DepreciationContext>> CreateContext(TokenModel token, DepreciationArgs args)
    {
        var period = await provider.GetFinancialPeriod(token, args.Id);
        if (period is null)
        {
            return OperateResult<DepreciationContext>.Faild($"未获取到期间信息：{args.Id}！");
        }
        if (period.Closed ?? false)
        {
            return OperateResult<DepreciationContext>.Faild($"{period.Period} 已记账！");
        }
        var (_, current) = await provider.GetFinancialInfo(token);
        if (current != period.Period)
        {
            return OperateResult<DepreciationContext>.Faild($"{period.Period} 月份不是当前月份！");
        }
        switch (args.Type)
        {
            case "Calculate":
            case "Confirm":
                if (period.depreciated ?? false)
                {
                    return OperateResult<DepreciationContext>.Faild($"{period.Period} 已折旧！");
                }
                if (args.Type == "Confirm")
                {
                    var coll = provider.TryGetCollection<BsonDocument>(token, "_system-things");
                    if (coll.success)
                    {
                        var filter = Builders<BsonDocument>.Filter.And(
                            Builders<BsonDocument>.Filter.Eq("belongId", token.BelongId.ToString()),
                            Builders<BsonDocument>.Filter.Eq("isDeleted", false),
                            Builders<BsonDocument>.Filter.Exists("locks")
                        );
                        var count = await coll.data.Find(filter).Limit(1).CountDocumentsAsync();
                        if (count > 0)
                        {
                            return OperateResult<DepreciationContext>.Faild($"存在锁定中资产，业务完结后可确认折旧！");
                        }
                    }
                    else
                    {
                        return OperateResult<DepreciationContext>.Faild($"获取 _system-things 集合失败！");
                    }
                }
                break;
            case "Revoke":
                if (!(period.depreciated ?? false))
                {
                    return OperateResult<DepreciationContext>.Faild($"{period.Period} 折旧未确认！");
                }
                break;
            case "Close":
                if (!(period.depreciated ?? false))
                {
                    return OperateResult<DepreciationContext>.Faild($"{period.Period} 折旧未确认！");
                }
                break;
        }
        if (period.operationId is not null)
        {
            var operation = await provider.GetOperationLog(token, period.operationId);
            if (operation is not null)
            {
                if (operation.status != (int)Status.Completed)
                {
                    return OperateResult<DepreciationContext>.Faild($"上一操作未完成！");
                }
            }
        }
        var config = await provider.GetDepreciationConfig(token);
        if (config is null || config.Check())
        {
            return OperateResult<DepreciationContext>.Faild("折旧配置不完整！");
        }
        config.Initialize();
        var context = new DepreciationContext(token, config, period, args);
        var result = DateTime.TryParse(current, out var businessTime);
        if (!result)
        {
            return OperateResult<DepreciationContext>.Faild($"当前业务时间: {current} 解析失败！");
        }
        context.BusinessTime = businessTime;
        var created = await provider.InsertOperation(context.Token, context.OperationLog);
        if (!created.success)
        {
            return OperateResult<DepreciationContext>.Faild(created.msg);
        }
        return OperateResult<DepreciationContext>.Success(context);
    }

    private async Task CalculateOrConfirm(DepreciationContext context, bool confirm)
    {
        var total = await CountQuery(context);
        await ClearChange(context);
        await ClearSnapshot(context);
        if (total > 0)
        {
            await StartDepreciation(context, confirm, total, async (skip) =>
            {
                await provider.UpdateOperationProgress(context.Token, context.OperationLog.id, skip, total);
            });
        }
        if (confirm)
        {
            await provider.UpdateDepreciationStatus(context.Token, context.Current.id, true);
        }
    }

    private async Task Revoke(DepreciationContext context)
    {
        var total = await WorkCountQuery(context);
        if (total > 0)
        {
            await RevokeDepreciation(context, async (skip) =>
            {
                await provider.UpdateOperationProgress(context.Token, context.OperationLog.id, skip, total);
            });
        }
        await ClearChange(context);
        await ClearSnapshot(context);
        await provider.UpdateDepreciationStatus(context.Token, context.Current.id, false);
    }

    private async Task<int> CountQuery(DepreciationContext context)
    {
        var loadOptions = new DataSourceLoadOptions() { options = context.Config.BuildQuery(context.Token), isCountQuery = true };
        var result = await provider.LoadAsync(context.Token, DataProvider.ThingCollName, loadOptions);
        return ((LoadResult)result.data).totalCount;
    }

    private async Task<int> WorkCountQuery(DepreciationContext context)
    {
        var loadOptions = new DataSourceLoadOptions() { options = context.BuildWorkQuery(), isCountQuery = true };
        var result = await provider.LoadAsync(context.Token, DataProvider.SnapshotCollName, loadOptions);
        return ((LoadResult)result.data).totalCount;
    }

    private async Task<OperateResult> ClearChange(DepreciationContext context)
    {
        return await provider.RemoveAsync(context.Token, DataProvider.ChangeCollName, context.BuildMatch());
    }

    private async Task<OperateResult> ClearSnapshot(DepreciationContext context)
    {
        return await provider.RemoveAsync(context.Token, DataProvider.SnapshotCollName, context.BuildMatch());
    }

    private async Task UpdateThings(DepreciationContext context, List<EntityBase> things)
    {
        var coll = provider.TryGetCollection<EntityBase>(context.Token, DataProvider.ThingCollName);
        if (coll.success)
        {
            foreach (var thing in things)
            {
                var filterUpdate = context.Config.BuildFilterUpdate(thing);
                if (filterUpdate is not null)
                {
                    var (filter, update) = filterUpdate.Value;
                    await coll.data.UpdateOneAsync(filter, update);
                }
            }
        }
    }

    private async Task StartDepreciation(DepreciationContext context, bool confirm, int totalCount, Func<int, Task> onSkip)
    {
        var comparer = new CompareContext(context.Config.Fields) { Token = context.Token, Instance = context.Instance, ChangeTime = context.Current.Period };
        int skip = 0, take = 300;
        while (skip < totalCount)
        {
            Thread.Sleep(10);
            var options = new DataSourceLoadOptions() { options = context.Config.BuildQuery(context.Token), take = take, skip = skip };
            var result = await provider.LoadAsync(context.Token, DataProvider.ThingCollName, options);
            if (result.success && result.data != null)
            {
                var oldThings = (result.data as LoadResult)?.data as List<EntityBase>;
                if (oldThings != null)
                {
                    var snapshots = new List<XSnapshot>();
                    var endThings = new List<EntityBase>();
                    var allChanges = new List<XChange>();
                    foreach (var before in oldThings)
                    {
                        var snapshot = new XSnapshot(before, context.Instance);
                        var (after, changes) = context.Config.Calculate(comparer, before, snapshot, context.BusinessTime);
                        if (after is not null)
                        {
                            endThings.Add(after);
                            snapshots.Add(snapshot);
                        }
                        allChanges.AddRange(changes);
                    }
                    if (snapshots?.Count > 0)
                    {
                        await provider.InsertAsync(context.Token, DataProvider.SnapshotCollName, JToken.FromObject(snapshots));
                    }
                    if (!confirm)
                    {
                        allChanges.ForEach(change => change.isDeleted = true);
                    }
                    if (allChanges?.Count > 0)
                    {
                        await provider.InsertAsync(context.Token, DataProvider.ChangeCollName, JToken.FromObject(allChanges));
                    }
                    if (confirm)
                    {
                        await UpdateThings(context, endThings);
                        await lockService.LockThings(context.Token, context.Instance, DataProvider.ThingCollName, endThings);
                    }
                    if (oldThings.Count < take)
                    {
                        await onSkip(totalCount);
                        break;
                    }
                    else
                    {
                        skip += take;
                        await onSkip(skip);
                    }
                }
            }
            else
            {
                throw new Exception(result.msg);
            }
        }
    }

    private async Task RevokeDepreciation(DepreciationContext context, Action<int> onSkip)
    {
        int skip = 0, take = 300;
        while (true)
        {
            var options = new DataSourceLoadOptions() { options = context.BuildWorkQuery(), take = take, skip = skip };
            var result = await provider.LoadAsync(context.Token, DataProvider.SnapshotCollName, options);
            if (!result.success || result.data is null || (result.data as LoadResult)?.data is not List<EntityBase> oldThings || oldThings.Count == 0)
            {
                break;
            }
            skip += oldThings.Count;

            oldThings.ForEach(thing => thing.id = thing["thingId"].ToString());
            await UpdateThings(context, oldThings);
            await lockService.UnLockThings(context.Token, DataProvider.ThingCollName, oldThings);

            onSkip(skip);
        }
    }


    private static DataSourceLoadOptions BuildOptions(CompareContext context, List<string> ids)
    {
        return new DataSourceLoadOptions()
        {
            options = new JObject()
            {
                ["match"] = new JObject()
                {
                    ["id"] = new JObject()
                    {
                        ["_in_"] = JArray.FromObject(ids)
                    },
                    ["belongId"] = context.Token.BelongId.ToString()
                }
            }
        };
    }
}
